package org.github.manx98.jlibhac.fs;

import cn.hutool.core.util.ByteUtil;
import org.github.manx98.jlibhac.common.math.Uint16;
import org.github.manx98.jlibhac.common.math.Uint32;
import org.github.manx98.jlibhac.common.math.Uint64;
import org.github.manx98.jlibhac.fs.fsa.IFile;
import org.github.manx98.jlibhac.fs.fsa.OpenMode;
import org.github.manx98.jlibhac.tools.fssystem.StorageFile;

import java.io.Closeable;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;

/**
 * @author Manx98
 * @version 1.0
 * @since 2022/10/2 22:41
 */
public abstract class IStorage implements Closeable {
    long position;

    public IStorage(long position) {
        this.position = position;
    }

    final public long getPosition() {
        return position;
    }

    final public void setPosition(long position) {
        if (position > getSize() || position < 0) {
            throw new IllegalArgumentException("invalid position " + position + ", position must be between 0 and " + getSize());
        }
        this.position = position;
    }

    final public byte[] readBytes(int size) throws IOException {
        long p = getPosition();
        byte[] bytes = new byte[size];
        read(p, bytes, 0, size);
        setPosition(p + size);
        return bytes;
    }

    public abstract void read(long offset, byte[] destination, int destOffset, int len) throws IOException;

    public abstract long getSize();

    @Override
    public void close() throws IOException {
        throw new UnsupportedOperationException("Not implemented!");
    }

    public long dryRead(long offset, int size, OpenMode openMode) {
        // Get the file size, and validate our offset.
        long fileSize = getSize();

        if (offset > fileSize) {
            throw new IllegalStateException("offset " + offset + " is out of file size range: " + fileSize);
        }
        return Math.min(fileSize - offset, size);
    }

    public int read(long offset, byte[] destination, int destOffset, OpenMode mode) throws IOException {
        int toRead = (int) dryRead(offset, destination.length - destOffset, mode);
        read(offset, destination, destOffset, toRead);
        return toRead;
    }

    public IStorage asStorage() {
        return new StreamStorage(this.slice(0));
    }

    public IStorage slice(long start) {
        long length = getSize();

        if (length == -1) {
            return slice(start, length);
        }

        return slice(start, length - start);
    }

    public IStorage slice(long start, long length) {
        return new SubStorage(this, start, length);
    }

    public byte readByte() throws IOException {
        return readBytes(1)[0];
    }

    public int read() throws IOException {
        return ByteUtil.byteToUnsignedInt(readByte());
    }

    public int readInt() throws IOException {
        byte[] bytes = readBytes(4);
        return ByteUtil.bytesToInt(bytes, ByteOrder.LITTLE_ENDIAN);
    }

    public long readLong() throws IOException {
        byte[] bytes = readBytes(8);
        return ByteUtil.bytesToLong(bytes, ByteOrder.LITTLE_ENDIAN);
    }

    public Uint16 readUint16() throws IOException {
        byte[] bytes = readBytes(2);
        return new Uint16(bytes);
    }

    public Uint32 readUint32() throws IOException {
        byte[] bytes = readBytes(4);
        return new Uint32(bytes);
    }

    public Uint64 readUint64() throws IOException {
        byte[] bytes = readBytes(8);
        return new Uint64(bytes);
    }

    public short readShort() throws IOException {
        byte[] bytes = readBytes(2);
        return ByteUtil.bytesToShort(bytes, ByteOrder.LITTLE_ENDIAN);
    }

    public String readAscii(int size) throws IOException {
        return new String(readBytes(size), StandardCharsets.US_ASCII);
    }

    public String readAsciiZ() throws IOException {
        int num = 0;
        long position = getPosition();
        int a;
        while (read() > 0) {
            num += 1;
        }
        setPosition(position);
        String text = readAscii(num);
        readBytes(1);
        return text;
    }

    public String readUtf8Z(int maxLength) throws IOException {
        long start = getPosition();
        int size = 0;
        // Read until we hit the end of the stream (-1) or a zero
        while (this.read() - 1 > 0 && size < maxLength) {
            size++;
        }
        setPosition(start);
        String text = readUtf8(size);
        readBytes(1);
        return text;
    }

    private String readUtf8(int size) throws IOException {
        return new String(readBytes(size), StandardCharsets.UTF_8);
    }

    public IFile asFile(OpenMode mode) {
        return new StorageFile(this, mode);
    }

    public Object toArray(Class<?> type) throws IOException {
        switch (type.getTypeName()) {
            case "int": {
                int count = Math.toIntExact(getSize() / 4);
                int[] result = new int[count];
                setPosition(0);
                for (int i = 0; i < count; ++i) {
                    result[i] = readInt();
                }
                return result;
            }
            case "byte": {
                setPosition(0);
                return readBytes(Math.toIntExact(getSize()));
            }
        }
        throw new IllegalArgumentException("Unsupported type: " + type.getName());
    }

    public void writeAllBytes(String filename) throws IOException {
        long size = getSize();
        try (FileOutputStream outputStream = new FileOutputStream(filename)) {
            while (size > 0) {
                outputStream.write(readBytes((int) Math.min(1024, size)));
                size -= 1024;
            }
        }
    }
}
